#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>

typedef struct sock_info {
	int fd;				//需要操作的文件描述符
	int ep_fd;			//epoll树实例
}s_info;
 
//子线程 连接客户端任务函数
void* accept_connect(void* arg) 
{
	printf("connect tid: %ld", pthread_self());

	s_info* info = (s_info*)arg;

	int cfd = accept(info->fd, NULL, NULL);
	//要求通讯描述符接收数据的操作不能阻塞的，就做不了其他事情了
	//我们可以把文件描述符修改为非阻塞状态，
	//这样读取数据的时候如果没有数据就不会阻塞在read/recv这里了
	int flag = fcntl(cfd, F_GETFL);
	flag |= O_NONBLOCK;
	fcntl(cfd, F_SETFL, flag);

	struct epoll_event ev;
	ev.events = EPOLLIN | EPOLLET;	//设置EPOLLET，则事件为边沿触发模式
	ev.data.fd = cfd;
	if (epoll_ctl(info->ep_fd, EPOLL_CTL_ADD, cfd, &ev) == -1)
	{
		perror("epoll_ctl error");
	}
	free(info);
	return NULL;
}

//子线程 通讯任务函数
void* communication(void* arg) 
{
	printf("communication tid: %ld", pthread_self());
	s_info* info = (s_info*)arg;

	char buff[5];
	char temp[1024];
	bzero(temp, sizeof(temp));

	//采用循环读取的方式读出文件描述符读缓冲区的数据
	while (1)
	{
		//因为我们设置了文件描述符为非阻塞类型，recv、read函数读完数据后会返回一个EAGAIN的标准信息到errno里面
		int len = recv(info->fd, buff, sizeof(buff), 0);
		if (len == -1)
		{
			// 判断recv是否是读完数据了
			if (errno == EAGAIN)
			{
				printf("recv end\n");
				if (send(info->fd, temp, sizeof(temp), 0) == -1)
				{
					perror("send error");
					break;
				}
				break;
			}
			else
			{
				perror("recv error");
				break;
			}
		}
		else if (len == 0)
		{
			printf("客户端断开连接。。。\n");
			if (epoll_ctl(info->ep_fd, EPOLL_CTL_DEL, info->fd, NULL) == -1)
			{
				perror("epoll_ctl error");
				break;
			}
			close(info->fd);
			break;
		}


		//小写转大写
		for (int i = 0; i < len; i++)
		{
			buff[i] = toupper(buff[i]);
		}
		strncat(temp + strlen(temp), buff, len);
		//使用printf的时候，发现读取到的字符串没有\0 所以打印输出的时候最后一个字符乱码，
		//可以利用write方法往控制台写多少个字符的方式解决这个问题
		write(STDOUT_FILENO, buff, len);
		//printf("after buff = %s\n", buff);
		
	}
	//释放内存资源
	free(info);
	return NULL;
}

int main()
{
	//第一步：创建监听的套接字
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd == -1)
	{
		perror("socket error");
		exit(1);
	}

	//第二步：绑定本地的IP 和 port
	struct sockaddr_in saddr;
	memset(&saddr, 0, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(9999);		
	saddr.sin_addr.s_addr = htonl(INADDR_ANY);

	//设置端口复用
	int op = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op));

	//绑定端口
	int res = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
	if (res == -1)
	{
		perror("bind error");
		exit(1);
	}

	//第三步：设置监听
	res = listen(fd, 64);
	if (res == -1)
	{
		perror("listen error");
		exit(1);
	}

	//创建epoll实例
	int ep_fd = epoll_create(1);
	if (ep_fd == -1)
	{
		perror("epoll_create error");
		exit(0);
	}

	//epoll事件监听有两种模式，分贝为水平触发模式和边沿触发模式，默认为水平触发模式；

	//水平触发模式：epoll检测到读/写事件就绪，
	//就会通知我们，一直到读/写缓冲区没有数据（未就绪）

	//边沿触发模式：epoll检测到读/写事件就绪，
	//只会通知我们一次，不管我们是否已经读/写缓冲区没有数据处理完
	struct epoll_event ev;
	ev.events = EPOLLIN | EPOLLET;	//设置EPOLLET，则事件为边沿触发模式
	ev.data.fd = fd;
	// 把监听的事件加入到epoll实例（树）里面
	if (epoll_ctl(ep_fd, EPOLL_CTL_ADD, fd, &ev) == -1)
	{
		perror("epoll_ctl error");
		exit(0);
	}

	struct epoll_event evs[1024];
	int size = sizeof(evs) / sizeof(evs[0]);
	while (1)
	{
		int num = epoll_wait(ep_fd, evs, size, -1);
		printf("num = %d\n", num);
		for (int i = 0; i < num; i++)
		{
			int t_fd = evs[i].data.fd;
			pthread_t tid;
			s_info* info = (s_info*)malloc(sizeof(s_info));
			info->fd = t_fd;
			info->ep_fd = ep_fd;
			//判断文件描述符是用来监听的还是用来通讯的
			if (t_fd == fd)
			{
				pthread_create(&tid, NULL, accept_connect, info);
				pthread_detach(tid);
			}
			else  
			{
				pthread_create(&tid, NULL, communication, info);
				pthread_detach(tid);
			}
		}
	}
	
	close(fd);
	return 0;
}